Passed
Push — depfu/update/npm/lodash-4.17.1... ( 152c97 )
by
unknown
04:58
created

SaveFileService.getChecksum   A

Complexity

Conditions 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
import { SaveFileIterator } from './savefile-expanded/SaveFileIterator';
2
import { SaveFileExpanded } from './savefile-expanded/SaveFileExpanded';
3
import { Injectable, NgZone } from '@angular/core';
4
import { TextService } from "./text.service";
5
import { GameDataService } from './gameData.service';
6
7
/**
8
   Copyright 2018 June Hanabi
9
10
   Licensed under the Apache License, Version 2.0 (the "License");
11
   you may not use this file except in compliance with the License.
12
   You may obtain a copy of the License at
13
14
       http://www.apache.org/licenses/LICENSE-2.0
15
16
   Unless required by applicable law or agreed to in writing, software
17
   distributed under the License is distributed on an "AS IS" BASIS,
18
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
   See the License for the specific language governing permissions and
20
   limitations under the License.
21
 */
22
23
/**
24
 * Provides means to work with a Pokemon Red save file and correctly extract
25
 * various data formats in a useable and readable form as well as write them
26
 * back correctly.
27
 */
28
29
declare var window: {
30
    saveFile: SaveFileService
31
    require: any
32
}
33
34
declare var Buffer: any;
35
36
const { ipcRenderer } = window.require('electron');
37
38
@Injectable({
39
    providedIn: 'root'
40
})
41
export class SaveFileService {
42
43
    constructor(public saveText: TextService,
44
        public gd: GameDataService,
45
        public ng: NgZone) {
46
        this.fileDataExpanded = new SaveFileExpanded(this);
47
        window.saveFile = this;
48
49
        ipcRenderer.on("dataChange", this.onDataChange.bind(this));
50
        ipcRenderer.on("dataUpdate", this.onDataUpdate.bind(this));
51
    }
52
53
    public get iterator(): SaveFileIterator {
54
        return new SaveFileIterator(this);
55
    }
56
57
    // @ts-ignore
58
    public onDataChange(sender: any, data: Uint8Array, internalOnly: boolean = false) {
59
        this.ng.run<any>(async () => {
60
            this.fileData = data;
61
62
            if (!internalOnly)
63
                this.fileDataExpanded = new SaveFileExpanded(this);
64
        });
65
    }
66
67
    public onDataUpdate() {
68
        this.ng.run<any>(async () => {
69
            // Write values back from the expanded file to this file
70
            this.fileDataExpanded.save(this);
71
72
            // Recalculate all checksums
73
            this.recalcChecksums();
74
75
            ipcRenderer.send("ipcFrom", "dataUpdate", this.fileData, true);
76
        });
77
    }
78
79
    /*
80
    * bcd2number -> takes a nodejs buffer with a BCD and returns the corresponding number.
81
    * input: nodejs buffer
82
    * output: number
83
    * credit: joaomaia @ https://gist.github.com/joaomaia/3892692
84
    */
85
    public bcd2number(bcd: Uint8Array): number {
86
        var n = 0;
87
        var m = 1;
88
        for (var i: number = 0; i < bcd.length; i += 1) {
89
            n += (bcd[bcd.length - 1 - i] & 0x0F) * m;
90
            n += ((bcd[bcd.length - 1 - i] >> 4) & 0x0F) * m * 10;
91
            m *= 100;
92
        }
93
        return n;
94
    }
95
96
    /*
97
    * number2bcd -> takes a number and returns the corresponding BCD in a nodejs buffer object.
98
    * input: 32 bit positive number, nodejs buffer size
99
    * output: nodejs buffer
100
    * credit: joaomaia @ https://gist.github.com/joaomaia/3892692
101
    */
102
    public number2bcd(number: number, size: number): Uint8Array {
103
        var s = size || 4; //default value: 4
104
        var bcd = new Buffer(s);
105
        bcd.fill(0);
106
        while (number !== 0 && s !== 0) {
107
            s -= 1;
108
            bcd[s] = (number % 10);
109
            number = (number / 10) | 0;
110
            bcd[s] += (number % 10) << 4;
111
            number = (number / 10) | 0;
112
        }
113
        return bcd;
114
    }
115
116
    // Copies a range of bytes to a buffer of size and returns them
117
    public getRange(from: number, size: number, bigEndian: boolean = false): Uint8Array {
118
        if (bigEndian)
119
            return this.fileData.subarray(from, from + size).reverse();
120
        else
121
            return this.fileData.subarray(from, from + size);
122
    }
123
124
    // Copies data to the save data at a particular place going no further
125
    // than the maximum size desired to be copied and the maximum array
126
    // given top copy from
127
    //
128
    // from = index to start copying at inclusive
129
    // size = maximum length to copy
130
    // data = array of data to copy into, will stop at size or data length
131
    public copyRange(from: number, size: number, data: Uint8Array, bigEndian: boolean = false) {
132
133
        if (bigEndian)
134
            data = data.reverse();
135
136
        for (let i = from, j = 0; j < data.length && i < (from + size); i++ , j++) {
137
            this.fileData[i] = data[j];
138
        }
139
    }
140
141
    // Gets a string from the save file auto-converted to normal characters
142
    // Strings are always little endian
143
    public getStr(from: number, size: number, maxChars: number): string {
144
        return this.saveText.convertFromCode(this.getRange(from, size), maxChars);
145
    }
146
147
    // Sets a string to save file auto-converted to in-game characters
148
    // Strings are always little endian
149
    public setStr(from: number, size: number, maxChars: number, str: string): void {
150
        this.copyRange(from, size, this.saveText.convertToCode(str, maxChars));
151
    }
152
153
    // Gets a value in hex from the save file
154
    public getHex(from: number, size: number, prefix: boolean = false, bigEndian: boolean = false): string {
155
156
        let rawHex = this.getRange(from, size, bigEndian);
157
158
        let hexStr = "";
159
        for (let i = 0; i < rawHex.length; i++) {
160
            hexStr += rawHex[i].toString(16);
161
        }
162
        hexStr = hexStr.toUpperCase();
163
        if (prefix)
164
            hexStr = `0x${hexStr}`;
165
166
        return hexStr;
167
    }
168
169
    // Saves a hex value to bytes in the save file
170
    public setHex(from: number, size: number, hex: string, hasPrefix: boolean = false, bigEndian: boolean = false): void {
171
        // Trim prefix if any
172
        if (hasPrefix)
173
            hex = hex.substr(2);
174
175
        // Convert to number
176
        let hexValue = parseInt(hex, 16);
177
        const hexValueOrig = hexValue;
178
        let hexArr = [];
179
180
        // Break apart number into seperate bytes and store them in an
181
        // array. This also places it big-endian style which is how the
182
        // save data is structured
183
184
        if (hexValue === 0)
185
            hexArr.push(0);
186
        else
187
            while (hexValue > 0) {
188
                hexArr.push(hexValue & 0xFF);
189
                hexValue >>= 8;
190
            }
191
192
        // Account for bug where 16-bit value under 0xFF still needs to take up
193
        // 16-bits
194
        if (hexValueOrig <= 0xFF && size == 2)
195
            hexArr.push(0x00);
196
197
        // Copy to save data
198
        this.copyRange(from, size, new Uint8Array(hexArr), !bigEndian);
199
    }
200
201
    // Get a Raw BCD value in integer format
202
    // BCD is always little endian
203
    public getBCD(from: number, size: number): number {
204
        return this.bcd2number(this.getRange(from, size));
205
    }
206
207
    // Sets a number in raw BCD format
208
    // BCD is always little endian
209
    public setBCD(from: number, size: number, val: number): void {
210
        this.copyRange(from, size, this.number2bcd(val, size));
211
    }
212
213
    // Gets a bit from a value
214
    public getBit(from: number, size: number, bit: number, bigEndian: boolean = false): boolean {
215
        let value = this.getRange(from, size);
216
217
        if (bigEndian)
218
            value = value.reverse();
219
220
        let res = value[0];
221
222
        if (size > 0)
223
            for (let i = 1; i < size; i++) {
224
                res <<= 8;
225
                res |= value[i];
226
            }
227
228
        return (res & (1 << bit)) !== 0;
229
    }
230
231
    // Sets/Resets a bit in a value
232
    public setBit(from: number, size: number, bit: number, val: boolean, bigEndian: boolean = false): void {
233
        let value = this.getRange(from, size, bigEndian);
234
235
        let res = value[0];
236
237
        if (size > 0)
238
            for (let i = 1; i < size; i++) {
239
                res <<= 8;
240
                res |= value[i];
241
            }
242
243
        if (val)
244
            res |= (1 << bit);
245
        else
246
            res &= ~(1 << bit);
247
248
        const resHex = res.toString(16);
249
        this.setHex(from, size, resHex, false, bigEndian);
250
    }
251
252
    public getWord(from: number, bigEndian: boolean = false): number {
253
        let value = this.getRange(from, 2, bigEndian);
254
255
        let res = value[0];
256
        res <<= 8;
257
        res |= value[1];
258
259
        return res;
260
    }
261
262
    public setWord(from: number, value: number, bigEndian: boolean = false): void {
263
264
        const byteL = value & 0x00FF;
265
        const byteH = (value & 0xFF00) >> 8;
266
267
        this.copyRange(from, 2, new Uint8Array([byteH, byteL]), bigEndian);
268
    }
269
270
    public getByte(offset: number): number {
271
        return this.fileData[offset];
272
    }
273
274
    public setByte(offset: number, value: number): void {
275
        this.fileData[offset] = value;
276
    }
277
278
    // Calculates checksum from start index inclusive to end index exclusive
279
    // and returns it
280
    public getChecksum(from: number, to: number): number {
281
        const toChecksum = this.fileData.subarray(from, to);
282
        const checksum = new Uint8Array([0xFF]);;
283
        for (let i = 0; i < toChecksum.length; i++) {
284
            checksum[0] -= toChecksum[i];
285
        }
286
        return checksum[0];
287
    }
288
289
    // Recalculates all checksums and sets them on the save data
290
    // Skips Pokemon box checksums unless marked as formatted or unless forced
291
    public recalcChecksums(force: boolean = false) {
292
        // Bank 1 Checksum
293
        this.fileData[0x3523] = this.getChecksum(0x2598, 0x3523);
294
295
        const boxesFormatted = (this.fileData[0x284C] & 0b10000000) > 0;
296
        if (boxesFormatted || force)
297
            this.recalcBoxesChecksums();
298
    }
299
300
    recalcBoxesChecksums() {
301
        // Bank 2 individual checksums for Boxes 1-6
302
        const bank2IndvChecksums = [
303
            this.getChecksum(0x4000, 0x4462),
304
            this.getChecksum(0x4462, 0x48C4),
305
            this.getChecksum(0x48C4, 0x4D26),
306
            this.getChecksum(0x4D26, 0x5188),
307
            this.getChecksum(0x5188, 0x55EA),
308
            this.getChecksum(0x55EA, 0x5A4C),
309
        ];
310
        this.copyRange(0x5A4D, 0x6, new Uint8Array(bank2IndvChecksums));
311
312
        // Bank 2 checksum excluding individual checksums
313
        // Similar to bank 1 checksum
314
        this.fileData[0x5A4C] = this.getChecksum(0x4000, 0x5A4C);
315
316
        // Bank 3 Checksums for Boxes 7-12
317
        const bank3IndvChecksums = [
318
            this.getChecksum(0x6000, 0x6462),
319
            this.getChecksum(0x6462, 0x68C4),
320
            this.getChecksum(0x68C4, 0x6D26),
321
            this.getChecksum(0x6D26, 0x7188),
322
            this.getChecksum(0x7188, 0x75EA),
323
            this.getChecksum(0x75EA, 0x7A4C),
324
        ];
325
        this.copyRange(0x7A4D, 0x6, new Uint8Array(bank3IndvChecksums));
326
327
        // Bank 3 Checksum
328
        this.fileData[0x7A4C] = this.getChecksum(0x6000, 0x7A4C);
329
    }
330
331
    // Buffered file data
332
    public fileData: Uint8Array = new Uint8Array(0x8000);
333
334
    // Expands the save data to something more readable and useable
335
    public fileDataExpanded: SaveFileExpanded;
336
}
337